//----------------------------------------------------------------------------
// Copyright (C) 2003  Rafael H. Bordini, Jomi F. Hubner, et al.
// 
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// 
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
// 
// To contact the authors:
// http://www.inf.ufrgs.br/~bordini
// http://www.das.ufsc.br/~jomi
//
//----------------------------------------------------------------------------

package jason.asSyntax;

import jason.JasonException;
import jason.asSemantics.*;
import jason.asSyntax.Trigger.TEOperator;
import jason.asSyntax.Trigger.TEType;
import jason.asSyntax.parser.ParseException;
import jason.bb.BeliefBase;

import java.util.*;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Represents a set of plans used by an agent
 * 
 * @has - plans 0..* Plan
 */
public class PlanLibrary implements Iterable<Plan> {
	
	/** a MAP from TE to a list of relevant plans */
	private Map<PredicateIndicator, List<Plan>> relPlans = new HashMap<PredicateIndicator, List<Plan>>();
	
	/**
	 * All plans as defined in the AS code (maintains the order of the plans)
	 */
	private List<Plan> plans = new ArrayList<Plan>();
	
	/** list of plans that have var as TE */
	private List<Plan> varPlans = new ArrayList<Plan>();
	
	/** A map from labels to plans */
	private Map<String, Plan> planLabels = new HashMap<String, Plan>();
	
	private static int lastPlanLabel = 0;
	
	/** The agent which owns this plan library. */
	private final Agent ag;
	
	/** The plans used during the execution of an atomic expression. */
	private DeltaPlanLibrary dpl;
	
	//private Logger logger = Logger.getLogger(PlanLibrary.class.getName());  
	
	public PlanLibrary(Agent ag) {
		this.ag = ag;
		this.dpl = new DeltaPlanLibrary(this);
	}
	
	/**
	 * Add a new plan written as a String. The source normally is "self" or the agent that sent this plan. If the PL
	 * already has a plan equals to "stPlan", only a new source is added.
	 * 
	 * The plan is added in the end of the PlanLibrary.
	 * 
	 * @returns the plan just added
	 * @deprecated parse the plan before (ASSyntax methods) and call add(Plan, ...) methods
	 */
	public Plan add(StringTerm stPlan, Term tSource) throws ParseException, JasonException {
		return add(stPlan, tSource, false);
	}
	
	/**
	 * Add a new plan written as a String. The source normally is "self" or the agent that sent this plan. If the PL
	 * already has a plan equals to "stPlan", only a new source is added.
	 * 
	 * If <i>before</i> is true, the plan will be added in the begin of the PlanLibrary; otherwise, it is added in the
	 * end.
	 * 
	 * @returns the plan just added
	 * @deprecated parse the plan before (ASSyntax methods) and call add(Plan, ...) methods
	 */
	public Plan add(StringTerm stPlan, Term tSource, boolean before) throws ParseException, JasonException {
		String sPlan = stPlan.getString();
		// remove quotes \" -> " ==> it's now process properly at the creating of the string term
		/*
		StringBuilder sTemp = new StringBuilder();
		for (int c=0; c <sPlan.length(); c++) {
		    if (sPlan.charAt(c) != '\\') {
		        sTemp.append(sPlan.charAt(c));
		    }
		}
		sPlan  = sTemp.toString();
		*/
		Plan p = ASSyntax.parsePlan(sPlan);
		return add(p, tSource, before);
	}
	
	/**
	 * Add a new plan in PL. The source normally is "self" or the agent that sent this plan. If the PL already has a
	 * plan equals to the parameter p, only a new source is added.
	 * 
	 * If <i>before</i> is true, the plan will be added in the begin of the PlanLibrary; otherwise, it is added in the
	 * end.
	 * 
	 * @returns the plan just added
	 */
	public Plan add(Plan p, Term source, boolean before) throws ParseException {
		if (ag.isAtomic()) {
			return dpl.add(p, source, before);
		}
		
		int i = plans.indexOf(p);
		if (i < 0) {
			// add label, if necessary
			if (p.getLabel() == null) p.setLabel(getUniqueLabel());
			p.getLabel().addSource(source);
			add(p, before);
		} else {
			p = plans.get(i);
			p.getLabel().addSource(source);
		}
		return p;
	}
	
	public void add(Plan p) throws ParseException {
		add(p, false);
	}
	
	/**
	 * Adds a plan into the plan library, either before or after all other plans depending on the boolean parameter.
	 * 
	 * @param p
	 *            The plan to be added to the plan library
	 * @param before
	 *            Whether or not to place the new plan before others
	 * @throws ParseException
	 *             if a plan already exists with the specified label
	 */
	public void add(Plan p, boolean before) throws ParseException {
		if (ag.isAtomic()) {
			dpl.add(p, before);
			return;
		}
		
		// test p.label
		if (p.getLabel() != null && planLabels.keySet().contains(p.getLabel().getFunctor())) {
			// test if the new plan is equal, in this case, just add a source
			Plan planInPL = get(p.getLabel().getFunctor());
			if (p.equals(planInPL)) {
				planInPL.getLabel().addSource(p.getLabel().getSources().get(0));
				return;
			} else {
				throw new ParseException("There already is a plan with label " + p.getLabel(), p.getToken());
			}
		}
		
		// add label, if necessary
		if (p.getLabel() == null) p.setLabel(getUniqueLabel());
		
		// add self source
		if (!p.getLabel().hasSource()) p.getLabel().addAnnot(BeliefBase.TSelf);
		
		p.setAsPlanTerm(false); // it is not a term anymore
		
		planLabels.put(p.getLabel().getFunctor(), p);
		
		Trigger pte = p.getTrigger();
		if (pte.getLiteral().isVar()) {
			if (before) varPlans.add(0, p);
			else varPlans.add(p);
			// add plan p in all entries
			for(List<Plan> lp : relPlans.values())
				if (!lp.isEmpty() && lp.get(0).getTrigger().sameType(pte)) // only add if same type
				if (before) lp.add(0, p);
				else lp.add(p);
		} else {
			List<Plan> codesList = relPlans.get(pte.getPredicateIndicator());
			if (codesList == null) {
				codesList = new ArrayList<Plan>();
				// copy plans from var plans
				for(Plan vp : varPlans)
					if (vp.getTrigger().sameType(pte)) codesList.add(vp);
				relPlans.put(pte.getPredicateIndicator(), codesList);
			}
			if (before) codesList.add(0, p);
			else codesList.add(p);
		}
		
		if (before) plans.add(0, p);
		else plans.add(p);
	}
	
	public void addAll(PlanLibrary pl) throws ParseException {
		for(Plan p : pl) {
			add(p, false);
		}
	}
	
	public void addAll(List<Plan> plans) throws ParseException {
		for(Plan p : plans) {
			add(p, false);
		}
	}
	
	/** add a label to the plan */
	Pred getUniqueLabel() {
		String l;
		do {
			l = "l__" + (lastPlanLabel++);
		} while (planLabels.keySet().contains(l));
		return new Pred(l);
	}
	
	/** return a plan for a label */
	public Plan get(String label) {
		if (ag.isAtomic()) {
			if (dpl.isIgnore(label)) {
				return dpl.get(label);
			}
			return planLabels.get(label);
		}
		return planLabels.get(label);
	}
	
	public int size() {
		if (ag.isAtomic()) {
			int size = 0;
			for(Plan p : plans) {
				if (!dpl.isIgnore(p)) ++size;
			}
			return size + dpl.size();
		}
		return plans.size();
	}
	
	public List<Plan> getPlans() {
		if (ag.isAtomic()) {
			ArrayList<Plan> pls = new ArrayList<Plan>();
			for(Plan p : plans) {
				if (!dpl.isIgnore(p)) {
					pls.add(p);
				}
			}
			pls.addAll(dpl.getPlans());
			return pls;
		}
		return plans;
	}
	
	public List<Plan> getPlansToTest() {
		return plans;
	}
	
	public Map<String, Plan> getLabels() {
		if (ag.isAtomic()) {
			HashMap<String, Plan> lbls = new HashMap<String, Plan>();
			for(Map.Entry<String, Plan> e : planLabels.entrySet()) {
				if (!dpl.isIgnore(e.getKey())) {
					lbls.put(e.getKey(), e.getValue());
				}
			}
			lbls.putAll(dpl.getLabels());
			return Collections.unmodifiableMap(lbls);
		}
		
		return Collections.unmodifiableMap(planLabels);
	}
	
	public Iterator<Plan> iterator() {
		if (ag.isAtomic()) {
			return new Iterator<Plan>() {
				private boolean needGet = true;
				private Iterator<Plan> it = plans.iterator();
				private byte phase = 0;
				private Plan next = null;
				
				public boolean hasNext() {
					if (needGet) get();
					return next != null;
				}
				
				public Plan next() {
					if (needGet) get();
					if (next != null) needGet = true;
					return next;
				}
				
				public void remove() {
					it.remove();
				}
				
				private void get() {
					needGet = false;
					
					labelSwitch: switch (phase) {
						case 0:
							while (it.hasNext()) {
								next = it.next();
								if (!dpl.isIgnore(next)) break labelSwitch;
							}
							phase = 1;
							it = dpl.iterator();
							//$FALL-THROUGH$
						case 1:
							if (it.hasNext()) {
								next = it.next();
							} else {
								next = null;
							}
							break;
					}
				}
			};
		}
		return plans.iterator();
	}
	
	/** remove all plans */
	public void clear() {
		planLabels.clear();
		plans.clear();
		varPlans.clear();
		relPlans.clear();
		deltaClear();
	}
	
	/** reset the delta plan library */
	public void deltaClear() {
		dpl.clear();
	}
	
	/** commit the delta plan library to this one */
	public void deltaCommit() {
		dpl.commit();
	}
	
	/**
	 * Remove a plan represented by the label <i>pLabel</i>. In case the plan has many sources, only the plan's source
	 * is removed.
	 */
	public boolean remove(Atom pLabel, Term source) {
		if (ag.isAtomic()) {
			return dpl.remove(pLabel, source);
		}
		// find the plan
		Plan p = get(pLabel.getFunctor());
		if (p != null) {
			boolean hasSource = p.getLabel().delSource(source);
			
			// if no source anymore, remove the plan
			if (hasSource && !p.getLabel().hasSource()) {
				remove(pLabel.getFunctor());
			}
			return true;
		}
		return false;
	}
	
	/** remove the plan with label <i>pLabel</i> */
	public Plan remove(String pLabel) {
		if (ag.isAtomic()) {
			return dpl.remove(pLabel);
		}
		
		Plan p = (Plan)planLabels.remove(pLabel);
		
		remove(p);
		return p;
	}
	
	private void remove(Plan p) {
		if (p == null) return;
		
		// remove it from plans' list
		plans.remove(p);
		
		if (p.getTrigger().getLiteral().isVar()) {
			varPlans.remove(p);
			// remove p from all entries
			for(List<Plan> lp : relPlans.values())
				lp.remove(p);
		} else {
			List<Plan> codesList = relPlans.get(p.getTrigger().getPredicateIndicator());
			if (codesList != null) {
				codesList.remove(p);
				if (codesList.isEmpty()) {
					// no more plans for this TE
					relPlans.remove(p.getTrigger().getPredicateIndicator());
				}
			}
		}
	}
	
	void replace(String label, Plan p) {
		planLabels.put(label, p);
		
		plansReplace(plans, label, p);
		
		if (p.getTrigger().getLiteral().isVar()) {
			plansReplace(varPlans, label, p);
			// replace p at all entries
			for(List<Plan> lp : relPlans.values()) {
				plansReplace(lp, label, p);
			}
		} else {
			List<Plan> codesList = relPlans.get(p.getTrigger().getPredicateIndicator());
			plansReplace(codesList, label, p);
		}
	}
	
	public static final void plansReplace(List<Plan> list, String label, Plan plan) {
		int max = list.size();
		int i = -1;
		
		for(int j = 0; j < max; ++j) {
			Plan p = list.get(j);
			if (p != null && p.getLabel().getFunctor().equals(label)) {
				i = j;
				break;
			}
		}
		
		if (i < 0) {
			list.add(plan);
			return;
		}
		
		list.remove(i);
		list.add(i, plan);
	}
	
	/** @deprecated use hasCandidatePlan(te) instead */
	public boolean isRelevant(Trigger te) {
		return hasCandidatePlan(te);
	}
	
	public boolean hasCandidatePlan(Trigger te) {
		List<Plan> l = getCandidatePlans(te);
		return l != null && !l.isEmpty();
	}
	
	/** @deprecated use getCandidatePlans(te) instead */
	public List<Plan> getAllRelevant(Trigger te) {
		return getCandidatePlans(te);
	}
	
	public List<Plan> getCandidatePlans(Trigger te) {
		if (ag.isAtomic()) {
			ArrayList<Plan> cpls = new ArrayList<Plan>();
			List<Plan> l = relPlans.get(te.getPredicateIndicator());
			if (l != null) for(Plan p : l) {
				if (!dpl.isIgnore(p)) {
					cpls.add(p);
				}
			}
			l = dpl.getRelPlans(te);
			if (l != null) cpls.addAll(l);
			
			if (cpls.isEmpty() && (!varPlans.isEmpty() || !dpl.getVarPlans().isEmpty())) { // no rel plan, try varPlan
				for(Plan p : varPlans) {
					if (!dpl.isIgnore(p)) {
						if (p.getTrigger().sameType(te)) cpls.add(p);
					}
				}
				
				for(Plan p : dpl.getVarPlans()) {
					if (p.getTrigger().sameType(te)) cpls.add(p);
				}
			}
			
			if(cpls.isEmpty()) return null;
			return cpls;
		}
		
		List<Plan> l = relPlans.get(te.getPredicateIndicator());
		if ((l == null || l.isEmpty()) && !varPlans.isEmpty()) { // no rel plan, try varPlan
			l = new ArrayList<Plan>();
			for(Plan p : varPlans)
				if (p.getTrigger().sameType(te)) l.add(p);
		}
		return l;
	}
	
	public static final String S_IDLE = "idle";
	public static final Trigger TE_IDLE = new Trigger(TEOperator.add, TEType.achieve, new Atom(S_IDLE));
	
	public List<Plan> getIdlePlans() {
		if (ag.isAtomic()) {
			ArrayList<Plan> ipls = new ArrayList<Plan>();
			List<Plan> l = relPlans.get(TE_IDLE.getPredicateIndicator());
			if (l != null) for(Plan p : l) {
				if (!dpl.isIgnore(p)) {
					ipls.add(p);
				}
			}
			l = dpl.getIdlePlans();
			if (l != null) ipls.addAll(l);
			
			if(ipls.isEmpty()) return null;
			return ipls;
		}
		return relPlans.get(TE_IDLE.getPredicateIndicator());
	}
	
	public PlanLibrary clone() {
		PlanLibrary pl = new PlanLibrary(ag);
		for(Plan p : plans) {
			try {
				pl.add((Plan)p.clone(), false);
			} catch (ParseException e) {
				e.printStackTrace();
			}
		}
		pl.dpl = dpl.clone();
		return pl;
	}
	
	public String toString() {
		return getPlans().toString();
	}
	
	/** get as XML */
	public Element getAsDOM(Document document) {
		Element eplans = (Element)document.createElement("plans");
		String lastFunctor = null;
		for(Plan p : plans) {
			String currentFunctor = p.getTrigger().getLiteral().getFunctor();
			if (lastFunctor != null && !currentFunctor.equals(lastFunctor)) {
				eplans.appendChild((Element)document.createElement("new-set-of-plans"));
			}
			lastFunctor = currentFunctor;
			eplans.appendChild(p.getAsDOM(document));
		}
		return eplans;
	}
}
